%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "The four C++ cast operators separate previously ambiguous cast semantics"
%%| fig-width: 6.4
%%| fig-height: 3.4
flowchart TB
Casts["C++ cast operators"]
Dyn["dynamic_cast<br/>runtime-checked hierarchy conversion"]
Stat["static_cast<br/>compile-time conversion"]
Const["const_cast<br/>change const/volatile qualifiers"]
Reint["reinterpret_cast<br/>reinterpret bits / addresses"]
Casts --> Dyn
Casts --> Stat
Casts --> Const
Casts --> Reint
W4. C++ Type Casts, Type Identification, Deleted and Defaulted Functions, Initializing Members, Delegating Constructors, this Pointer, Constant Member Functions
1. Summary
1.1 The Problem with C-Style Type Casts
Before C++11, programmers used C-style casts for all type conversions. However, these casts are too generic and lack semantic clarity.
1.1.1 Traditional Casting Notation
C++ inherited two casting syntaxes from C:
C-style notation:
int x = (int)12.34;Functional notation:
int x = int(12.34);Both of these forms are overly generic and can perform fundamentally different operations with the same syntax:
int x = (int)12.34; // Value conversion: modifies bits (rounding)
int* px = &x;
long a = (long)px; // Reinterpretation: doesn't modify bits
Derived* pd = new Derived();
Base* pb = (Base*)pd; // Upcasting: needs runtime check1.1.2 The Semantic Problem
The fundamental issue with traditional casts is ambiguity of intent. When you see (Type)expression, it’s unclear what kind of conversion is happening:
- Value conversion? (e.g.,
double→intwith data loss) - Reinterpretation? (e.g., pointer → integer, no bit modification)
- Upcasting/downcasting? (e.g.,
Derived*→Base*) - Adding/removing constness? (e.g.,
const char*→char*)
This ambiguity makes code harder to read, maintain, and debug. You cannot easily search for specific types of casts, and the compiler cannot provide appropriate warnings.
1.1.3 The C++ Solution
C++ introduced four specialized cast operators, each with a specific purpose and clear semantics:
dynamic_cast<T>(v)- Safe runtime type conversion with checksstatic_cast<T>(v)- Compile-time type conversion without runtime checksconst_cast<T>(v)- Add or remove const/volatile qualifiersreinterpret_cast<T>(v)- Reinterpret bit patterns without modification
Each cast operator has a distinct purpose, making code more self-documenting and allowing the compiler to catch errors.
1.2 Static and Dynamic Types Revisited
Before diving into cast operators, let’s review an essential concept:
Static type is the type specified in your source code - determined at compile time:
Circle circle;
Shape* figure = &circle;Here, the static type of figure is Shape* (as declared).
Dynamic type is the actual type of the object at runtime - what the pointer/reference actually points to:
After the assignment above, the dynamic type of figure is Circle* (the actual object type).
This distinction is crucial for understanding when to use dynamic_cast vs static_cast.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Static type vs dynamic type"
%%| fig-width: 6
%%| fig-height: 3
flowchart LR
Decl["Shape* figure"]
Obj["actual object: Circle"]
Decl -- "static type" --> Shape["Shape*"]
Decl -- "dynamic type at runtime" --> Obj
1.3 Dynamic Cast
dynamic_cast<T>(v) performs runtime-checked conversions between pointers or references in an inheritance hierarchy.
1.3.1 Basic Syntax and Requirements
dynamic_cast<T>(v)Requirements:
Tmust be a pointer or reference typevmust be a pointer or reference to an object of a class type- The base class must have at least one virtual method (this enables RTTI - Runtime Type Information)
1.3.2 How Dynamic Cast Works
For pointers: Returns nullptr if the cast fails (object is not of the target type).
Base* pb = new Derived();
Derived* pd = dynamic_cast<Derived*>(pb);
if (pd != nullptr) {
// Cast succeeded: pb actually points to a Derived object
pd->derivedMethod();
} else {
// Cast failed: pb doesn't point to a Derived object
}For references: Throws std::bad_cast exception if the cast fails.
Base& rb = /* some base reference */;
try {
Derived& rd = dynamic_cast<Derived&>(rb);
// Cast succeeded
} catch (std::bad_cast& e) {
// Cast failed
}1.3.3 When to Use Dynamic Cast
Use dynamic_cast when:
- You need to safely downcast from a base class pointer/reference to a derived class
- You’re unsure of the actual runtime type of an object
- You need runtime type checking to prevent errors
Example:
class Base {
public:
virtual void f() { } // At least one virtual function required
};
class Derived : public Base {
public:
void derivedMethod() { cout << "Derived!" << endl; }
};
Base* pb = new Derived();
// C-style cast: DANGEROUS - no runtime check
Derived* pd1 = (Derived*)pb; // If pb doesn't point to Derived, undefined behavior!
// Dynamic cast: SAFE - runtime check performed
Derived* pd2 = dynamic_cast<Derived*>(pb);
if (pd2 != nullptr) {
pd2->derivedMethod(); // Safe to call
}Key advantage: dynamic_cast performs runtime checks. If pb doesn’t actually point to a Derived object, it returns nullptr instead of causing undefined behavior.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "dynamic_cast checks the runtime type before allowing a downcast"
%%| fig-width: 6.2
%%| fig-height: 3.2
classDiagram
class Base
class Derived
Base <|-- Derived
1.3.4 Performance Consideration
dynamic_cast has runtime overhead because it must check the object’s actual type. For performance-critical code where you’re certain of the type, consider static_cast instead (but be careful!).
1.4 Static Cast
static_cast<T>(v) performs compile-time conversions without runtime checks - faster but less safe than dynamic_cast.
1.4.1 Basic Syntax and Requirements
static_cast<T>(v)Requirements:
Tcan be a pointer, reference, or primitive typevcan be a pointer, reference, or value- Does not require virtual methods in the base class
1.4.2 How Static Cast Works
static_cast performs conversions that the compiler can verify at compile time, but without runtime safety checks:
Base* pb = new Derived();
Derived* pd1 = (Derived*)pb; // C-style: no checks
Derived* pd2 = static_cast<Derived*>(pb); // Same as above but more explicitThe difference from dynamic_cast:
- No runtime checks - the cast always succeeds
- Faster - no runtime overhead
- Dangerous - if
pbdoesn’t actually point toDerived, the behavior is undefined
1.4.3 Common Use Cases
1. Numeric conversions:
double d = 3.14;
int i = static_cast<int>(d); // Explicit conversion with potential data loss2. Upcasting (safe):
Derived* pd = new Derived();
Base* pb = static_cast<Base*>(pd); // Always safe (derived to base)3. Downcasting (dangerous):
Base* pb = /* ... */;
Derived* pd = static_cast<Derived*>(pb); // Only safe if you're CERTAIN pb points to Derived4. Void pointer conversions:
void* vp = /* ... */;
int* ip = static_cast<int*>(vp);1.4.4 When to Use Static Cast
Use static_cast when:
- You’re performing numeric conversions and want to be explicit about potential data loss
- You’re certain of the dynamic type (e.g., you just created the object)
- Performance is critical and you can guarantee type safety
- You need to cast to/from void pointers
Rule of thumb: If you’re not 100% certain of the type, use dynamic_cast instead.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "dynamic_cast vs static_cast for downcasting"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart LR
BasePtr["Base* pb"]
Dyn["dynamic_cast<Derived*>(pb)<br/>safe, checked"]
Stat["static_cast<Derived*>(pb)<br/>fast, unchecked"]
BasePtr --> Dyn
BasePtr --> Stat
1.5 Const Cast
const_cast<T>(v) adds or removes const (or volatile) qualifiers. This is the only cast that can remove constness.
1.5.1 Basic Syntax
const_cast<T>(v)- Used to remove or add
constorvolatilequalifiers - No actual operations are performed (compile-time construct)
- Does not change the bit pattern - purely a type system operation
1.5.2 Common Use Case: Interfacing with Legacy Code
Sometimes you need to pass a const object to a function that takes a non-const pointer but doesn’t actually modify it:
const char* str = "abcdef";
void legacyFunction(char* s); // Old API: doesn't modify s, but isn't marked const
// Error: can't pass const char* to char*
// legacyFunction(str);
// OK: remove constness with const_cast
legacyFunction(const_cast<char*>(str));⚠️ Warning: This is unsafe if legacyFunction actually modifies the data:
- If the original object was truly const (like a string literal), modifying it causes undefined behavior
- Only use
const_castwhen you’re certain the function won’t modify the data
1.5.3 Safe vs. Unsafe Usage
Safe usage:
const int* constPtr = /* ... */;
void readOnly(int* p) {
cout << *p; // Only reads, doesn't modify
}
// Safe if readOnly truly doesn't modify
readOnly(const_cast<int*>(constPtr));Unsafe usage:
const int x = 42;
int* px = const_cast<int*>(&x);
*px = 100; // UNDEFINED BEHAVIOR! x was originally const1.5.4 When to Use Const Cast
Use const_cast when:
- Interfacing with legacy C APIs that don’t use const correctly
- You have a const object but need to call a non-const method that you know doesn’t modify state
- You’re implementing const-correctness incrementally in a large codebase
Best practice: Avoid const_cast when possible. If you control the code, fix the function signature to be const-correct instead.
1.6 Reinterpret Cast
reinterpret_cast<T>(v) reinterprets the bit pattern of an object without modifying it - the most dangerous cast.
1.6.1 Basic Syntax and Purpose
reinterpret_cast<T>(v)- Changes interpretation of the binary representation
- No actual operations are performed (no bit modification)
- No runtime checks are performed
- Allows you to treat a value as if it were a completely different type
1.6.2 Common Use Cases
1. Storing pointers as integers:
int x = 777;
int* p = &x;
// Convert pointer to integer (e.g., for hashing or low-level debugging)
long internal = reinterpret_cast<long>(p);
// Convert back to pointer
int* back = reinterpret_cast<int*>(internal);2. Type-punning (viewing same memory as different types):
unsigned int bits = 0x41200000; // Bit pattern
float* f = reinterpret_cast<float*>(&bits);
// Now *f interprets those bits as a float3. Converting between incompatible pointer types:
unsigned* px = /* ... */;
int* py = reinterpret_cast<int*>(px); // OK with reinterpret_castWithout reinterpret_cast, this would be an error:
unsigned* px = /* ... */;
int* py = px; // ERROR: incompatible types1.6.3 Dangers and Warnings
reinterpret_cast is extremely dangerous:
- Bypasses type safety completely
- Can cause undefined behavior if the reinterpretation is invalid
- Platform-dependent (pointer sizes, endianness, alignment)
Example of danger:
unsigned x = 777;
unsigned* px = &x;
int y = 999;
int* py = &y;
py = reinterpret_cast<int*>(px); // "OK" syntax-wise
*py = -1; // Undefined behavior! Writing signed value to unsigned memory1.6.4 When to Use Reinterpret Cast
Use reinterpret_cast only when:
- Interfacing with low-level system APIs
- Implementing custom memory allocators
- Working with hardware registers or memory-mapped I/O
- Serialization/deserialization requiring raw memory access
For most application-level code, you should never need reinterpret_cast.
1.7 Type Identification with typeid
The typeid operator allows you to identify types at runtime, complementing dynamic_cast for RTTI (Run-Time Type Information).
1.7.1 Basic Syntax
typeid(expression) // Type of an expression
typeid(type) // Type itselfThe typeid operator returns a reference to a std::type_info object containing information about the type.
Similarity to sizeof:
Just as sizeof can work with both types and expressions:
sizeof(int) // Size of type
sizeof(x) // Size of expression's typetypeid works the same way:
typeid(int) // Type info for int
typeid(x) // Type info for x's type1.7.2 The type_info Class
From ISO C++ Standard, Section 17.7.3:
namespace std {
class type_info {
public:
virtual ~type_info();
bool operator==(const type_info& rhs) const noexcept;
bool before(const type_info& rhs) const noexcept;
size_t hash_code() const noexcept;
const char* name() const noexcept;
type_info(const type_info&) = delete; // Cannot be copied
type_info& operator=(const type_info&) = delete; // Cannot be copied
};
}Key operations:
- Get the type name:
name()returns a string representation - Compare types:
operator==checks if two types are the same - Hash the type:
hash_code()for use in hash tables
Note: Nobody really knows what before() means (it’s implementation-defined), so it’s rarely used.
1.7.3 Using typeid
Checking dynamic types:
Base* pb = new Derived();
const std::type_info& info = typeid(*pb); // Dynamic type (Derived)
cout << "Type: " << info.name() << endl;Comparing types:
Base* pb = new Derived();
if (typeid(*pb) == typeid(Derived)) {
cout << "pb points to a Derived object" << endl;
}
if (typeid(*pb) == typeid(Base)) {
cout << "pb points to a Base object" << endl; // Won't print
}Important distinction:
Base* pb = new Derived();
typeid(pb) // Type: Base* (static type of the pointer)
typeid(*pb) // Type: Derived (dynamic type of the object)1.7.4 When to Use typeid
Use typeid when:
- You need to know the actual runtime type of an object
- Implementing custom serialization/reflection systems
- Debugging (printing type information)
- Implementing type-based dispatch without dynamic_cast
However: Overusing typeid can indicate poor OOP design. Prefer polymorphism (virtual functions) over explicit type checking.
1.7.5 Comparison: typeid vs dynamic_cast
| Feature | typeid |
dynamic_cast |
|---|---|---|
| Purpose | Identify type | Convert type |
| Returns | type_info reference |
Pointer/reference or nullptr |
| Use case | Type checking | Safe downcasting |
| Performance | Fast | Slower (traverses inheritance) |
Often you can use either:
// Using typeid
if (typeid(*pb) == typeid(Derived)) {
Derived* pd = static_cast<Derived*>(pb);
pd->derivedMethod();
}
// Using dynamic_cast (preferred)
if (Derived* pd = dynamic_cast<Derived*>(pb)) {
pd->derivedMethod();
}The dynamic_cast approach is generally preferred as it’s more idiomatic C++.
1.8 Deleted and Defaulted Functions
C++11 introduced explicit control over special member functions using = default and = delete specifiers.
1.8.1 The Problem: Automatic Generation Rules
The compiler can automatically generate certain special member functions:
class T {
// Compiler may generate:
T(); // Default constructor
T(const T&); // Copy constructor
T(T&&); // Move constructor
virtual ~T(); // Destructor
T& operator=(const T&); // Copy assignment
T&& operator=(T&&); // Move assignment
};However, the rules for when these are generated are extremely complicated:
- If you define any constructor, the default constructor is not generated
- If you define a move constructor, copy operations are deleted
- If you define a destructor, copy assignment generation is “not recommended”
- Many other complex rules…
The problem: These rules are hard to remember and can lead to subtle bugs.
1.8.2 Motivation: Non-Copyable Objects
A common idiom is creating objects that cannot be copied:
Old approach (before C++11):
class NonCopyable {
public:
NonCopyable() { }
private:
// Declare but don't define - causes linker error if called
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};Problems with this approach:
- Intent unclear: Why are these private? To prevent copying or for another reason?
- Linker errors: If a friend function tries to copy, you get a linker error (not compile error)
- Must define default constructor: Because defining any constructor suppresses default generation
1.8.3 The Solution: = delete
Modern approach with = delete:
class NonCopyable {
public:
NonCopyable() = default; // Generate default constructor
NonCopyable(const NonCopyable&) = delete; // Delete copy constructor
NonCopyable& operator=(const NonCopyable&) = delete; // Delete copy assignment
};Advantages:
- Clear intent: Explicitly states these operations are forbidden
- Compile-time errors: Attempting to copy gives immediate compile error
- No need for empty constructor body:
= defaultgenerates it automatically - Works for any function: Not just special members
1.8.4 Using = default
Example 1: Forcing default constructor generation
class A {
public:
A(int x) { } // Defining this suppresses default constructor
};
A a; // ERROR: no default constructorSolution:
class A {
public:
A(int x) { }
A() = default; // Force generation of default constructor
};
A a; // OK nowExample 2: Explicit about generated functions
Even when the compiler would generate a function, explicitly defaulting it makes intent clear:
class C {
public:
C() = default; // Explicit: this class is default-constructible
C(const C&) = default; // Explicit: this class is copyable
C& operator=(const C&) = default; // Explicit: this class is copy-assignable
~C() = default; // Explicit: destructor is not virtual
};1.8.5 Using = delete for More Than Special Members
Preventing heap allocation:
class StackOnly {
public:
void* operator new(size_t) = delete; // Prevent heap allocation
};
StackOnly* ps = new StackOnly(); // ERROR: operator new is deleted
StackOnly s; // OK: stack allocationPreventing unwanted conversions:
void foo(double x) { /* ... */ }
foo(3.14); // OK: double literal
foo(3); // OK: int converts to double
foo(true); // OK: bool converts to double (unintended!)To allow only double:
void foo(double x) { /* ... */ }
void foo(int) = delete; // Block int
void foo(bool) = delete; // Block bool
foo(3.14); // OK
foo(3); // ERROR: deleted function
foo(true); // ERROR: deleted functionEven more restrictive - allow ONLY double:
template<typename T>
void foo(T) = delete; // Delete ALL other types
void foo(double x) { /* ... */ } // Only this overload allowed
foo(3.14); // OK: exact match for double
foo(3); // ERROR: would instantiate deleted template
foo(2.71F); // ERROR: float would instantiate deleted template1.8.6 Best Practices
- Be explicit: Use
= defaultand= deleteto clearly state intent - Don’t rely on automatic generation rules: They’re too complex
- Use for any function: Not just special members
- Prefer compile-time errors:
= deletegives better error messages than private declarations
1.9 Initializing Bases and Members
When creating derived class objects, you must properly initialize both the base class subobject and the derived class members.
1.9.1 The Constructor Execution Order
When a derived class object is created, constructors execute in this order:
- Base class constructor (initializes base subobject)
- Member initializers (in declaration order)
- Derived class constructor body
class Base {
int m;
public:
Base() { m = 0; }
Base(int i) { m = i; }
};
class Derived : public Base {
int md;
public:
Derived() { md = 7; }
};
Derived d; // What happens?Question: Which Base constructor is called?
1.9.2 The Problem
The base class might have multiple constructors. How do you specify which one to use?
class Derived : public Base {
public:
Derived() { md = 7; } // Which Base constructor is called?
int md;
};Default behavior: If you don’t specify, the base class default constructor is called. But what if there is no default constructor, or you want to call a different one?
1.9.3 Constructor Initializer Lists (Ctor-initializer)
The constructor initializer list (also called member initializer list) specifies which base constructor to call and how to initialize members:
class Derived : public Base {
public:
Derived() : Base(1), md(7) { }
// ^^^^^^^^ ^^^^^
// | Member initializer
// Base class initializer
int md;
};Syntax:
DerivedConstructor(parameters) : BaseClass(args), member1(value1), member2(value2) {
// Constructor body
}1.9.4 Initializing Base Classes
Basic example:
class Base {
public:
Base() { m = 0; }
Base(int i) { m = i; }
int m;
};
class Derived : public Base {
public:
Derived() : Base(1) { md = 7; } // Call Base(int) constructor
int md;
};
Derived d; // Calls Base(1), then initializes md = 7Multiple parameters:
class Base {
public:
Base(int x, int y) { /* ... */ }
};
class Derived : public Base {
public:
Derived() : Base(10, 20) { /* ... */ }
};1.9.5 Initializing Data Members
You can (and should) initialize members in the initializer list:
class C {
int m1;
int m2;
T m3; // Some class type
public:
C() : m1(5), m2(10), m3(15) { } // Member initialization
};Why use initializer lists for members?
- More efficient: Members are directly initialized (not default-constructed then assigned)
- Required for: Const members, reference members, members without default constructors
- Clearer: Separates initialization from other logic
1.9.6 Initialization vs. Assignment
Method 1: Assignment in body (less efficient)
class C {
int md;
T md2; // Some class type
public:
C() {
md = 7; // Assignment (for primitive types, OK)
md2 = T(5); // Default-construction, then assignment (inefficient)
}
};Method 2: Initialization in list (preferred)
class C {
int md;
T md2;
public:
C() : md(7), md2(5) { // Direct initialization
}
};For class types, Method 1 performs two operations:
- Default-construct
md2 - Assign
T(5)tomd2
Method 2 performs one operation:
- Directly construct
md2with value 5
1.9.7 Required Initialization Cases
Const members:
class C {
const T md2;
public:
C() { md2 = expression; } // ERROR: cannot assign to const
C() : md2(expression) { } // OK: initialization
};Reference members:
class C {
int& ref;
public:
C(int& r) : ref(r) { } // Must use initializer list
};Members without default constructors:
class NoDefault {
public:
NoDefault(int x) { } // No default constructor
};
class C {
NoDefault nd;
public:
C() : nd(42) { } // Must initialize in list
};1.10 Delegating Constructors
Delegating constructors (C++11) allow one constructor to call another constructor of the same class, reducing code duplication.
1.10.1 The Problem: Code Duplication
Often, multiple constructors perform common initialization:
class C {
int x, y;
public:
C() {
// Common initialization
x = 0;
y = 0;
// Specific logic
}
C(int val) {
// Common initialization (duplicated!)
x = 0;
y = 0;
// Specific logic
x = val;
}
};Old solution: Extract common code to a private method:
class C {
int x, y;
private:
void init() { // Common initialization
x = 0;
y = 0;
}
public:
C() {
init();
// Specific actions
}
C(int val) {
init();
// Specific actions
x = val;
}
};1.10.2 The Modern Solution: Delegating Constructors
Instead of a separate init() method, one constructor can delegate to another:
class C {
int x, y;
public:
C() { // Target constructor
x = 0;
y = 0;
}
C(int val) : C() { // Delegating constructor
x = val; // Specific actions
}
};Syntax: In the initializer list, call another constructor: ConstructorName(args)
1.10.3 Execution Order
When a delegating constructor is called:
- The target constructor executes completely (including its body)
- Then the delegating constructor’s body executes
class C {
public:
C() {
cout << "Common initialization" << endl;
}
C(int x) : C() {
cout << "Specific initialization" << endl;
}
};
C c(42);
// Output:
// Common initialization
// Specific initialization1.10.4 Terminology
class C {
public:
C(int) { } // Target constructor
C() : C(42) { } // Delegating constructor
};- Target constructor: The constructor being called
- Delegating constructor: The constructor that delegates to another
- Primary constructor: A common design pattern where one constructor does the main initialization, and others delegate to it
1.10.5 Rules and Restrictions
Cannot mix delegation with member initialization:
class C {
int x;
public:
C(int val) : x(val) { }
C() : C(0), x(10) { } // ERROR: cannot delegate and initialize members
};Cannot create circular delegation:
class C {
public:
C(int) { }
C(): C(42) { } // Delegates to C(int)
C(char c): C(42.0) { } // ERROR: circular delegation
C(double d): C('a') { } // ERROR: circular delegation
};Delegation must be exclusive:
If you delegate, you cannot also:
- Initialize base classes
- Initialize members
- Call other constructors
1.10.6 Benefits
- Reduce duplication: Common initialization code in one place
- Better maintainability: Changes to common initialization only needed once
- Clearer intent: Explicit that one constructor builds on another
1.11 The this Pointer
Inside member functions, the special pointer this points to the object on which the member function was called.
1.11.1 What is this?
this is an implicit parameter available in all non-static member functions:
class C {
int member;
public:
void f(int i) {
member = i; // Implicitly: this->member = i
this->member = i; // Explicitly using this
}
};By definition, these are equivalent:
member = i;
this->member = i;1.11.2 Why this is Needed
1. Disambiguating names:
When a parameter has the same name as a member:
class Point {
double x, y;
public:
void setX(double x) {
this->x = x; // this->x is the member, x is the parameter
}
};2. Returning the object itself:
Useful for method chaining:
class Builder {
public:
Builder& setWidth(int w) {
width = w;
return *this; // Return reference to current object
}
Builder& setHeight(int h) {
height = h;
return *this;
}
private:
int width, height;
};
Builder b;
b.setWidth(10).setHeight(20); // Method chaining3. Passing the object to other functions:
void externalFunction(C* obj);
class C {
public:
void f() {
externalFunction(this); // Pass pointer to current object
}
};4. Checking for self-assignment:
class C {
public:
C& operator=(const C& other) {
if (this != &other) { // Check if assigning to self
// Perform assignment
}
return *this;
}
};1.11.3 Type of this
For a class C:
- In a non-const member function:
thishas typeC* const(constant pointer to C) - In a const member function:
thishas typeconst C* const(constant pointer to const C)
Why this is a constant pointer:
C* const this; // Implicit declarationThis prevents you from changing what this points to:
class C {
public:
void bad() {
this = &other; // ERROR: cannot modify this
}
};1.11.4 How Member Functions Actually Work
Member functions receive this as a hidden first parameter:
class C {
public:
int m;
void f(int i) { m = 7; }
};The compiler transforms this to something like:
void f(C* this, int i) { // Hidden 'this' parameter
this->m = 7;
}When you call a member function:
C c;
c.f(1); // Becomes: f(&c, 1)
C* p = new C();
p->f(1); // Becomes: f(p, 1)The compiler automatically passes the address of the object as the first argument.
1.11.5 Static Member Functions
Static member functions do not have a this pointer because they don’t belong to any instance:
class C {
int member;
static int sMember;
public:
static void f() {
member = 5; // ERROR: no 'this' pointer
sMember = 7; // OK: static member
}
};1.12 Constant Member Functions
Constant member functions promise not to modify the object’s state - they treat this as a pointer to const.
1.12.1 The const Qualifier
Add const after the parameter list to declare a const member function:
class C {
int member;
public:
void f1() { // Non-const member function
member = 5; // OK: can modify
}
void f2() const { // Const member function
member = 5; // ERROR: cannot modify
int x = member; // OK: can read
}
};1.12.2 The Type of this in Const Member Functions
Regular member function:
void f() {
// this has type: C* const
// Can modify object through this
}Const member function:
void f() const {
// this has type: const C* const
// Cannot modify object through this
}The const qualifier changes this from a pointer-to-modifiable to a pointer-to-const.
1.12.3 When Const Member Functions Are Required
Const member functions can be called on const objects; non-const functions cannot:
class C {
public:
void f1() { }
void f2() const { }
};
C c1;
c1.f1(); // OK
c1.f2(); // OK
const C c2;
c2.f1(); // ERROR: f1 can modify c2
c2.f2(); // OK: f2 cannot modify c2Why this matters: When you pass objects by const reference (a common pattern for efficiency), you can only call const member functions:
void process(const C& obj) {
obj.f1(); // ERROR: f1 is not const
obj.f2(); // OK: f2 is const
}1.12.4 Best Practice: Mark Methods const When Possible
If a member function doesn’t modify the object’s state, mark it const:
class Point {
double x, y;
public:
double getX() const { return x; } // Doesn't modify, should be const
double getY() const { return y; }
void setX(double newX) { x = newX; } // Modifies, cannot be const
};Benefits:
- Documents intent: Clearly states the function doesn’t modify state
- Enables use with const objects: Function can be called on const objects
- Better compiler optimization: Compiler knows the object won’t change
- Interface flexibility: Makes your class more usable in const contexts
1.12.5 Const Overloading
You can have two versions of the same function - one const, one non-const:
class C {
int* data;
public:
// Non-const version
int* getData() {
return data; // Returns modifiable pointer
}
// Const version
const int* getData() const {
return data; // Returns const pointer
}
};
C c1;
int* p1 = c1.getData(); // Calls non-const version
const C c2;
const int* p2 = c2.getData(); // Calls const versionThe compiler chooses based on whether the object is const.
1.13 Function Declaration vs. Definition
Understanding the distinction between declaration and definition is crucial for organizing C++ code into header and source files.
1.13.1 Declarations and Definitions
Declaration: Introduces a name and its type to the compiler.
Definition: Provides the complete implementation.
For functions:
int f(int x); // Declaration (function prototype)
int f(int x) { ... } // Definition (includes body)For classes:
class C; // Declaration (forward declaration)
class C { ... }; // Definition (complete class)1.13.2 Header and Source Files
C++ projects typically separate declarations and definitions:
Header file (Library.h):
// Declarations only
int f(int x);
class C {
int member;
public:
void method(int x); // Declaration
};Source file (Library.cpp):
#include "Library.h"
// Definitions
int f(int x) {
return x * 2;
}
void C::method(int x) { // Note: C::method syntax
member = x;
}User code (main.cpp):
#include "Library.h"
int main() {
f(42);
C obj;
obj.method(10);
}1.13.3 Member Function Definitions Outside Class
When defining member functions outside the class, use the scope resolution operator:
// In header
class C {
public:
void f(int x); // Declaration
};
// In source file
void C::f(int x) { // C::f specifies this is a member of C
// Implementation
}The :: tells the compiler:
“This f is not a free function; it’s the f that belongs to class C.”
1.13.4 Why Separate Declaration and Definition?
1. Compilation independence:
- Header contains the interface
- Multiple source files can include the same header
- Source files compile independently
- Only recompile files that changed
2. Encapsulation:
- Users see only the interface (header)
- Implementation details hidden in source file
- Can change implementation without affecting users
3. Reduced dependencies:
- Headers are small, compile quickly
- Large implementations don’t slow down every compilation
2. Definitions
- C-style cast: Traditional C casting syntax
(Type)expressionorType(expression)that performs any type conversion without clear semantic distinction. - Dynamic cast:
dynamic_cast<T>(v)- performs runtime-checked conversions in inheritance hierarchies, returning nullptr (for pointers) or throwing an exception (for references) if the cast fails. - Static cast:
static_cast<T>(v)- performs compile-time conversions without runtime checks, faster but potentially unsafe if the type is incorrect. - Const cast:
const_cast<T>(v)- adds or removes const/volatile qualifiers; the only cast that can remove constness. - Reinterpret cast:
reinterpret_cast<T>(v)- reinterprets the bit pattern of an object as a different type without modifying bits; the most dangerous cast. - Static type: The type specified in source code, determined at compile time (e.g.,
Shape*for aShape*pointer). - Dynamic type: The actual type of the object at runtime (e.g.,
Circlewhen aShape*points to aCircleobject). - Type identification: The process of determining an object’s type at runtime using
typeid. typeidoperator: Returns a reference to astd::type_infoobject containing runtime type information about an expression or type.std::type_info: Standard library class containing runtime type information; supports comparison and provides type name.- RTTI (Runtime Type Information): Compiler-generated information about object types, required for
dynamic_castandtypeidto work; requires at least one virtual function. = default: Specifier that explicitly requests compiler generation of a special member function with default behavior.= delete: Specifier that explicitly prohibits use of a function, generating compile-time errors if called.- Automatic generation: Compiler’s implicit creation of special member functions (default constructor, copy constructor, move constructor, destructor, copy/move assignment) under certain conditions.
- Constructor initializer list: Syntax after constructor parameters (
: base(args), member(value)) for initializing base classes and members before the constructor body executes. - Member initialization list: The portion of a constructor initializer list that initializes data members.
- Base class initialization: Specification in a constructor initializer list of which base class constructor to call.
- Delegating constructor: A constructor that calls another constructor of the same class in its initializer list to reuse initialization logic.
- Target constructor: The constructor being called by a delegating constructor.
thispointer: Implicit pointer available in non-static member functions that points to the object on which the member function was called; type isC* constorconst C* const.- Constant member function: Member function declared with
constqualifier that promises not to modify the object’s state;thisbecomes pointer-to-const. - Const overloading: Having two versions of a member function, one const and one non-const, with the compiler selecting based on object’s constness.
- Function declaration: Introduction of a function’s name and signature without providing implementation.
- Function definition: Complete specification of a function including its body/implementation.
- Scope resolution operator (
::): Operator used to specify class membership for out-of-class member function definitions (ClassName::functionName). - Header file: File (typically
.hor.hpp) containing declarations (interfaces) that multiple source files can include. - Source file: File (typically
.cpp) containing definitions (implementations) of functions and member functions. - Forward declaration: Declaration of a class name without full definition (
class ClassName;), allowing pointers/references before complete definition.
3. Examples
3.1. Bank Account Class - Complete Implementation (Lab 4, Task 1)
Implement a complete bank account system demonstrating:
- Base class
Accountwith basic operations - Derived class
SavingsAccountwith interest calculation - Use of
thispointer - Constant member functions
- Deleted functions for copy prevention
- Defaulted constructor
Click to see the solution
Key Concept: Building a complete class hierarchy demonstrating all major C++ class features from this lecture.
#include <iostream>
#include <string>
using namespace std;
class Account {
private:
int accountNumber;
double balance;
string ownerName;
public:
// Defaulted default constructor
Account() = default;
// Parameterized constructor
Account(int accNum, double initialBalance, string owner)
: accountNumber(accNum), balance(initialBalance), ownerName(owner) {
cout << "Account created for " << ownerName << endl;
}
// Deleted copy constructor and assignment operator
Account(const Account&) = delete;
Account& operator=(const Account&) = delete;
// Deposit money (uses this pointer for demonstration)
void deposit(double amount) {
if (amount > 0) {
this->balance += amount; // Explicit use of this
cout << "Deposited: $" << amount << endl;
} else {
cout << "Invalid deposit amount" << endl;
}
}
// Withdraw money (ensures balance doesn't go negative)
void withdraw(double amount) {
if (amount > 0 && this->balance >= amount) {
this->balance -= amount;
cout << "Withdrawn: $" << amount << endl;
} else {
cout << "Invalid withdrawal or insufficient funds" << endl;
}
}
// Constant member functions (don't modify state)
double getBalance() const {
return balance;
}
int getAccountNumber() const {
return accountNumber;
}
string getOwnerName() const {
return ownerName;
}
// Virtual destructor for proper inheritance
virtual ~Account() {
cout << "Account destroyed for " << ownerName << endl;
}
};
class SavingsAccount : public Account {
private:
double interestRate; // Annual interest rate (e.g., 2.5 for 2.5%)
public:
// Constructor delegating to base class
SavingsAccount(int accNum, double initialBalance, string owner, double rate)
: Account(accNum, initialBalance, owner), interestRate(rate) {
cout << "SavingsAccount created with " << interestRate << "% interest" << endl;
}
// Calculate and deposit interest
void calculateInterest() {
double interest = this->getBalance() * (interestRate / 100.0);
cout << "Calculating interest: $" << interest << endl;
this->deposit(interest); // Use inherited deposit method
}
// Constant member function
double getInterestRate() const {
return interestRate;
}
~SavingsAccount() {
cout << "SavingsAccount destroyed" << endl;
}
};
int main() {
cout << "=== Creating Savings Account ===" << endl;
SavingsAccount savings(123456, 1000.0, "John Doe", 2.5);
cout << "\n=== Initial State ===" << endl;
cout << "Account Number: " << savings.getAccountNumber() << endl;
cout << "Owner's Name: " << savings.getOwnerName() << endl;
cout << "Current Balance: $" << savings.getBalance() << endl;
cout << "Interest Rate: " << savings.getInterestRate() << "%" << endl;
cout << "\n=== Performing Transactions ===" << endl;
savings.deposit(500.0);
savings.withdraw(200.0);
cout << "\n=== After Transactions ===" << endl;
cout << "Current Balance: $" << savings.getBalance() << endl;
cout << "\n=== Calculating Interest ===" << endl;
savings.calculateInterest();
cout << "\n=== Final State ===" << endl;
cout << "Final Balance: $" << savings.getBalance() << endl;
// Attempting to copy would cause compile error
// SavingsAccount copy = savings; // ERROR: copy constructor deleted
// Account acc2 = savings; // ERROR: copy constructor deleted
cout << "\n=== Exiting (destructors called) ===" << endl;
return 0;
}Output:
=== Creating Savings Account ===
Account created for John Doe
SavingsAccount created with 2.5% interest
=== Initial State ===
Account Number: 123456
Owner's Name: John Doe
Current Balance: $1000
Interest Rate: 2.5%
=== Performing Transactions ===
Deposited: $500
Withdrawn: $200
=== After Transactions ===
Current Balance: $1300
=== Calculating Interest ===
Calculating interest: $32.5
Deposited: $32.5
=== Final State ===
Final Balance: $1332.5
=== Exiting (destructors called) ===
SavingsAccount destroyed
Account destroyed for John Doe
Explanation:
- Defaulted constructor:
Account() = defaultexplicitly generates default constructor - Deleted functions: Copy constructor and assignment deleted to prevent account duplication
- this pointer:
- Used explicitly in
deposit()andwithdraw()for clarity this->balanceaccesses member through pointer
- Used explicitly in
- Constant member functions:
getBalance(),getAccountNumber(),getOwnerName()marked const- Can be called on const Account objects
- Promise not to modify object state
- Inheritance:
SavingsAccountinherits fromAccount- Uses base class constructor in initializer list
- Extends functionality with interest calculation
- Virtual destructor:
- Ensures proper cleanup when deleting through base pointer
- Destructors called in reverse order (derived, then base)
Design decisions:
- Accounts cannot be copied (unique resource)
- Balance can only be modified through deposit/withdraw (encapsulation)
- Const member functions allow reading state without modification risk
- Interest calculation uses existing deposit method (code reuse)
Answer: Complete implementation demonstrating defaulted/deleted functions, this pointer usage, const correctness, proper initialization, and inheritance.
3.2. Shape Hierarchy with Type Casting (Lab 4, Task 2)
Implement a shape hierarchy and demonstrate the four different cast types in practical scenarios.
Click to see the solution
Key Concept: Using the appropriate cast type for different scenarios in an inheritance hierarchy.
#include <iostream>
#include <cmath>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // Pure virtual
virtual double perimeter() const = 0; // Pure virtual
virtual ~Shape() { } // Virtual destructor
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) { }
double area() const override {
return width * height;
}
double perimeter() const override {
return 2 * (width + height);
}
// Rectangle-specific method
double diagonal() const {
return sqrt(width * width + height * height);
}
double getWidth() const { return width; }
double getHeight() const { return height; }
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) { }
double area() const override {
return M_PI * radius * radius;
}
double perimeter() const override {
return 2 * M_PI * radius;
}
// Circle-specific method
double diameter() const {
return 2 * radius;
}
double getRadius() const { return radius; }
};
int main() {
Rectangle rectangle(5.0, 3.0);
Circle circle(4.0);
Shape* shape = &rectangle;
cout << "=== Demonstrate static casting [1] ===" << endl;
// Static cast: Compile-time downcast (no runtime check)
// Safe here because we KNOW shape points to Rectangle
const Rectangle* rectPtr = static_cast<const Rectangle*>(shape);
cout << "Rectangle width: " << rectPtr->getWidth() << endl;
cout << "Rectangle height: " << rectPtr->getHeight() << endl;
cout << "Rectangle diagonal: " << rectPtr->diagonal() << endl;
cout << "\n=== Demonstrate dynamic casting [2] ===" << endl;
// Dynamic cast: Runtime type checking
// Check if shape is actually a Circle
if (const Circle* circPtr = dynamic_cast<const Circle*>(shape)) {
cout << "Shape is a Circle with radius: " << circPtr->getRadius() << endl;
} else {
cout << "Shape is NOT a Circle" << endl;
}
// Now point to circle and check again
shape = &circle;
if (const Circle* circPtr = dynamic_cast<const Circle*>(shape)) {
cout << "Shape is a Circle with radius: " << circPtr->getRadius() << endl;
cout << "Circle diameter: " << circPtr->diameter() << endl;
} else {
cout << "Shape is NOT a Circle" << endl;
}
cout << "\n=== Demonstrate const casting [3] ===" << endl;
// Const cast: Remove const qualifier
// WARNING: Only safe if the original object wasn't const
const Rectangle* constRectPtr = &rectangle;
// Need to modify through a const pointer (normally not allowed)
// Remove const to call non-const method
Rectangle* mutableRectPtr = const_cast<Rectangle*>(constRectPtr);
// Now can call non-const methods (if they existed)
cout << "Successfully removed const (use with caution!)" << endl;
cout << "Area: " << mutableRectPtr->area() << endl;
cout << "\n=== Demonstrate reinterpret casting [4] ===" << endl;
// Reinterpret cast: Low-level bit reinterpretation
int intValue = 42;
// Treat the integer's bits as if they were a double
// WARNING: This is just for demonstration - meaningless operation!
double* doublePtr = reinterpret_cast<double*>(&intValue);
cout << "Integer value: " << intValue << endl;
cout << "Integer address: " << &intValue << endl;
cout << "Reinterpreted as double pointer: " << doublePtr << endl;
// Don't dereference doublePtr - it doesn't point to a valid double!
// More practical use: store pointer as integer
Shape* shapePtr = &rectangle;
long ptrAsInt = reinterpret_cast<long>(shapePtr);
cout << "Pointer stored as integer: " << ptrAsInt << endl;
// Convert back
Shape* restoredPtr = reinterpret_cast<Shape*>(ptrAsInt);
cout << "Restored pointer area: " << restoredPtr->area() << endl;
cout << "\n=== Summary ===" << endl;
cout << "1. static_cast: Fast compile-time cast (use when type is known)" << endl;
cout << "2. dynamic_cast: Safe runtime cast (use when type is uncertain)" << endl;
cout << "3. const_cast: Remove const (use rarely, with caution)" << endl;
cout << "4. reinterpret_cast: Bit reinterpretation (use for low-level operations)" << endl;
return 0;
}Output:
=== Demonstrate static casting [1] ===
Rectangle width: 5
Rectangle height: 3
Rectangle diagonal: 5.83095
=== Demonstrate dynamic casting [2] ===
Shape is NOT a Circle
Shape is a Circle with radius: 4
Circle diameter: 8
=== Demonstrate const casting [3] ===
Successfully removed const (use with caution!)
Area: 15
=== Demonstrate reinterpret casting [4] ===
Integer value: 42
Integer address: 0x7ffeefbff5ac
Reinterpreted as double pointer: 0x7ffeefbff5ac
Pointer stored as integer: 140732920755372
Restored pointer area: 15
=== Summary ===
1. static_cast: Fast compile-time cast (use when type is known)
2. dynamic_cast: Safe runtime cast (use when type is uncertain)
3. const_cast: Remove const (use rarely, with caution)
4. reinterpret_cast: Bit reinterpretation (use for low-level operations)
Explanation:
- static_cast [1]:
- Downcast from
Shape*toconst Rectangle* - Compile-time cast - no runtime checking
- Safe here because we know
shapepoints toRectangle - Faster than
dynamic_castbut requires programmer certainty
- Downcast from
- dynamic_cast [2]:
- Runtime type checking
- Returns
nullptrif cast fails (object is not of target type) - First check fails (shape points to Rectangle, not Circle)
- Second check succeeds (shape now points to Circle)
- Safer but has runtime overhead
- const_cast [3]:
- Removes const qualification
- Allows calling non-const methods through const pointer
- Only safe if original object wasn’t const
- Use sparingly - indicates potential design issue
- reinterpret_cast [4]:
- Reinterprets bit pattern without conversion
- Used to store pointer as integer and restore it
- Most dangerous cast - bypasses type system
- Use only for low-level operations
When to use each:
- Use
static_castwhen you’re certain of the type (performance-critical code) - Use
dynamic_castwhen you need runtime safety (uncertain about type) - Use
const_castwhen interfacing with legacy APIs (avoid if possible) - Use
reinterpret_castfor system programming only (almost never in application code)
Answer: Demonstrated all four cast types in practical scenarios. static_cast for known-safe conversions, dynamic_cast for runtime-safe downcasts, const_cast for const removal, reinterpret_cast for low-level operations.
3.3. Implementing a Non-Copyable Class (Lecture 4, Example 1)
Create a class that can be instantiated but cannot be copied, demonstrating proper use of = default and = delete.
Click to see the solution
Key Concept: Use = delete to explicitly prevent copying, and = default to explicitly request compiler-generated functions.
#include <iostream>
using namespace std;
class UniqueResource {
private:
int* data;
int id;
static int nextId;
public:
// Default constructor - explicitly defaulted
UniqueResource() = default;
// Parameterized constructor
UniqueResource(int value) : data(new int(value)), id(nextId++) {
cout << "Resource " << id << " created with value " << value << endl;
}
// Copy constructor - explicitly deleted
UniqueResource(const UniqueResource&) = delete;
// Copy assignment - explicitly deleted
UniqueResource& operator=(const UniqueResource&) = delete;
// Move constructor - can still move unique resources
UniqueResource(UniqueResource&& other) noexcept
: data(other.data), id(other.id) {
other.data = nullptr;
cout << "Resource " << id << " moved" << endl;
}
// Move assignment
UniqueResource& operator=(UniqueResource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
id = other.id;
other.data = nullptr;
cout << "Resource " << id << " move-assigned" << endl;
}
return *this;
}
// Destructor
~UniqueResource() {
if (data != nullptr) {
cout << "Resource " << id << " destroyed" << endl;
delete data;
} else {
cout << "Resource " << id << " (moved-from) destroyed" << endl;
}
}
void print() const {
if (data != nullptr) {
cout << "Resource " << id << ": " << *data << endl;
} else {
cout << "Resource " << id << ": (moved-from state)" << endl;
}
}
};
int UniqueResource::nextId = 1;
int main() {
cout << "=== Creating resources ===" << endl;
UniqueResource r1(42);
UniqueResource r2(100);
cout << "\n=== Printing resources ===" << endl;
r1.print();
r2.print();
// Copy operations are deleted
// UniqueResource r3 = r1; // ERROR: copy constructor deleted
// r2 = r1; // ERROR: copy assignment deleted
cout << "\n=== Moving resource ===" << endl;
UniqueResource r3 = std::move(r1); // OK: move constructor
cout << "\n=== After move ===" << endl;
r1.print(); // r1 is in moved-from state
r3.print(); // r3 now owns the resource
cout << "\n=== Exiting (destructors called) ===" << endl;
return 0;
}Output:
=== Creating resources ===
Resource 1 created with value 42
Resource 2 created with value 100
=== Printing resources ===
Resource 1: 42
Resource 2: 100
=== Moving resource ===
Resource 1 moved
=== After move ===
Resource 1: (moved-from state)
Resource 1: 42
=== Exiting (destructors called) ===
Resource 2 destroyed
Resource 1 destroyed
Resource 1 (moved-from) destroyed
Explanation:
- Explicit deletion:
- Copy constructor deleted:
UniqueResource(const UniqueResource&) = delete - Copy assignment deleted:
operator=(const UniqueResource&) = delete - Attempting to copy causes compile-time error
- Copy constructor deleted:
- Move semantics:
- Move constructor and assignment are still allowed
- Enables transferring ownership without copying
- Clear intent:
- Code explicitly documents that this class represents a unique resource
- Anyone reading the class definition immediately understands it’s non-copyable
- Compile-time enforcement:
- Errors caught at compilation, not runtime
- Better error messages than old private-declaration approach
Common use cases for non-copyable classes:
- File handles (only one handle should own a file)
- Network connections (copying a connection doesn’t make sense)
- Thread objects (threads are unique)
- Smart pointers like
std::unique_ptr
Answer: Explicitly delete copy operations with = delete to prevent copying. Move operations can still be provided for transfer of ownership.
3.4. Constructor Initializer Lists (Lecture 4, Example 2)
Demonstrate proper use of constructor initializer lists for both base class initialization and member initialization.
Click to see the solution
Key Concept: Constructor initializer lists initialize base classes and members before the constructor body executes, which is more efficient and sometimes required.
#include <iostream>
#include <string>
using namespace std;
class Base {
protected:
int m1, m2;
public:
Base() : m1(0), m2(0) {
cout << "Base default constructor" << endl;
}
Base(int a, int b) : m1(a), m2(b) {
cout << "Base(int, int) constructor: m1=" << m1 << ", m2=" << m2 << endl;
}
};
class Derived : public Base {
private:
int md;
const int constMember;
string name;
public:
// Using initializer list to specify base constructor and initialize members
Derived(int a, int b, int d, string n)
: Base(a, b), // Initialize base class
md(d), // Initialize member
constMember(999), // Initialize const member (REQUIRED)
name(n) // Initialize string member
{
cout << "Derived constructor: md=" << md
<< ", constMember=" << constMember
<< ", name=" << name << endl;
}
void print() const {
cout << "Base: m1=" << m1 << ", m2=" << m2 << endl;
cout << "Derived: md=" << md << ", constMember=" << constMember
<< ", name=" << name << endl;
}
};
// Example showing why initializer lists are required for certain members
class RequiresInitializerList {
private:
const int constValue; // Must be initialized
int& refValue; // Must be initialized
string str; // Has default constructor but more efficient to initialize
public:
// ALL of these MUST use initializer list
RequiresInitializerList(int val, int& ref, string s)
: constValue(val), // const: must be initialized, cannot be assigned
refValue(ref), // reference: must be initialized, cannot be rebound
str(s) // more efficient than default-construct + assign
{
// This would not work:
// constValue = val; // ERROR: cannot assign to const
// refValue = ref; // ERROR: cannot rebind reference
// str = s; // Works but less efficient (default construct + assign)
}
void print() const {
cout << "constValue=" << constValue
<< ", refValue=" << refValue
<< ", str=" << str << endl;
}
};
int main() {
cout << "=== Creating Derived object ===" << endl;
Derived d(10, 20, 30, "MyObject");
d.print();
cout << "\n=== Creating RequiresInitializerList object ===" << endl;
int x = 42;
RequiresInitializerList r(100, x, "Hello");
r.print();
// Modify x to show reference works
x = 999;
cout << "\nAfter modifying x:" << endl;
r.print();
return 0;
}Output:
=== Creating Derived object ===
Base(int, int) constructor: m1=10, m2=20
Derived constructor: md=30, constMember=999, name=MyObject
Base: m1=10, m2=20
Derived: md=30, constMember=999, name=MyObject
=== Creating RequiresInitializerList object ===
constValue=100, refValue=42, str=Hello
After modifying x:
constValue=100, refValue=999, str=Hello
Explanation:
- Base class initialization:
Base(a, b)in initializer list specifies which base constructor to call- Base constructor executes first, before derived members are initialized
- Member initialization:
- Members initialized in the order they’re declared in the class, not the order in the initializer list
- More efficient than assignment in constructor body (especially for class types)
- Required cases:
- Const members: Can only be initialized, not assigned
- Reference members: Must be bound at initialization
- Members without default constructors: Must be explicitly initialized
- Efficiency:
- Initializer list: Direct initialization
- Constructor body assignment: Default construction + assignment (two steps)
Common mistake - wrong initialization order:
class Wrong {
int b;
int a;
public:
Wrong() : a(5), b(a + 1) { } // WRONG! b is initialized before a
};Members are always initialized in declaration order, not initializer list order. Here, b is initialized before a (because b is declared first), so b gets an undefined value of a.
Answer: Use initializer lists to: 1) specify base constructor, 2) initialize const/reference members, 3) improve efficiency. Initialization order follows declaration order.
3.5. Delegating Constructors (Lecture 4, Example 3)
Demonstrate using delegating constructors to reduce code duplication when multiple constructors share common initialization logic.
Click to see the solution
Key Concept: Delegating constructors allow one constructor to call another, centralizing common initialization logic.
#include <iostream>
#include <string>
using namespace std;
class Rectangle {
private:
double width;
double height;
string color;
static int objectCount;
int id;
// Private helper for common initialization
void logCreation() {
cout << "Rectangle #" << id << " created: "
<< width << "x" << height << " (" << color << ")" << endl;
}
public:
// Target constructor - performs the main initialization
Rectangle(double w, double h, string c)
: width(w), height(h), color(c), id(++objectCount) {
logCreation();
}
// Delegating constructor - creates a square
Rectangle(double side)
: Rectangle(side, side, "white") { // Delegates to target constructor
cout << " (Created as square)" << endl;
}
// Delegating constructor - default color
Rectangle(double w, double h)
: Rectangle(w, h, "white") { // Delegates to target constructor
cout << " (Used default color)" << endl;
}
// Delegating constructor - default rectangle
Rectangle()
: Rectangle(1.0, 1.0, "white") { // Delegates to target constructor
cout << " (Default 1x1 rectangle)" << endl;
}
double area() const {
return width * height;
}
void print() const {
cout << "Rectangle #" << id << ": " << width << "x" << height
<< ", color=" << color << ", area=" << area() << endl;
}
};
int Rectangle::objectCount = 0;
int main() {
cout << "=== Creating rectangles with different constructors ===" << endl;
cout << "\n1. Full specification:" << endl;
Rectangle r1(5.0, 3.0, "red");
cout << "\n2. Width and height only (default color):" << endl;
Rectangle r2(4.0, 2.0);
cout << "\n3. Square (single dimension):" << endl;
Rectangle r3(3.0);
cout << "\n4. Default constructor:" << endl;
Rectangle r4;
cout << "\n=== Printing all rectangles ===" << endl;
r1.print();
r2.print();
r3.print();
r4.print();
return 0;
}Output:
=== Creating rectangles with different constructors ===
1. Full specification:
Rectangle #1 created: 5x3 (red)
2. Width and height only (default color):
Rectangle #2 created: 4x2 (white)
(Used default color)
3. Square (single dimension):
Rectangle #3 created: 3x3 (white)
(Created as square)
4. Default constructor:
Rectangle #4 created: 1x1 (white)
(Default 1x1 rectangle)
=== Printing all rectangles ===
Rectangle #1: 5x3, color=red, area=15
Rectangle #2: 4x2, color=white, area=8
Rectangle #3: 3x3, color=white, area=9
Rectangle #4: 1x1, color=white, area=1
Explanation:
- Target constructor:
Rectangle(double, double, string)contains all the initialization logic - Delegating constructors: Other constructors delegate to the target:
Rectangle(double)- creates a square by passing same value twiceRectangle(double, double)- uses default color “white”Rectangle()- uses all defaults
- Execution order:
- Target constructor executes completely (including body)
- Then delegating constructor’s body executes
- Benefits:
- Common initialization logic in one place
- Easy to maintain - changes only needed in target constructor
- Clear hierarchy of constructors
Alternative without delegation (old style):
class Rectangle {
// ...
private:
void init(double w, double h, string c) {
width = w;
height = h;
color = c;
id = ++objectCount;
logCreation();
}
public:
Rectangle(double w, double h, string c) { init(w, h, c); }
Rectangle(double side) { init(side, side, "white"); }
Rectangle(double w, double h) { init(w, h, "white"); }
Rectangle() { init(1.0, 1.0, "white"); }
};The delegating constructor approach is cleaner and more idiomatic in modern C++.
Answer: Delegating constructors call another constructor via initializer list (: ConstructorName(args)), centralizing initialization logic and reducing duplication.
3.6. C-Style Casts: The Problem (Tutorial 4, Example 1)
Analyze the problems with C-style casts and understand why C++ introduced specialized cast operators.
Click to see the solution
Key Concept: C-style casts are too generic and hide the programmer’s intent, making code harder to understand and maintain.
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { }
};
class Derived : public Base {
public:
void derivedMethod() { cout << "Derived method" << endl; }
};
int main() {
cout << "=== Problem 1: Value Conversion ===" << endl;
// Standard conversion: double → int (data loss)
int x = (int)12.34;
cout << "Converted 12.34 to int: " << x << endl;
cout << "Intent: Value conversion with rounding" << endl;
cout << "\n=== Problem 2: Pointer Reinterpretation ===" << endl;
// Reinterpretation: pointer → long (no bit modification)
int* px = &x;
long a = (long)px;
cout << "Pointer value as long: " << a << endl;
cout << "Intent: View pointer bits as integer" << endl;
cout << "\n=== Problem 3: Unsafe Upcasting ===" << endl;
// Downcasting without runtime checks
Derived* pd = new Derived();
Base* pb = (Base*)pd;
cout << "Upcasted Derived* to Base*" << endl;
cout << "Intent: Type hierarchy navigation (needs safety check)" << endl;
cout << "\n=== The Problem ===" << endl;
cout << "All three use identical syntax: (Type)expression" << endl;
cout << "But they do COMPLETELY DIFFERENT things:" << endl;
cout << " 1. Value conversion (modifies bits)" << endl;
cout << " 2. Reinterpretation (keeps bits, changes view)" << endl;
cout << " 3. Type hierarchy navigation (needs runtime check)" << endl;
cout << "\nSolution: Use specific cast operators!" << endl;
delete pb;
return 0;
}Output:
=== Problem 1: Value Conversion ===
Converted 12.34 to int: 12
Intent: Value conversion with rounding
=== Problem 2: Pointer Reinterpretation ===
Pointer value as long: 140732920755324
Intent: View pointer bits as integer
=== Problem 3: Unsafe Upcasting ===
Upcasted Derived* to Base*
Intent: Type hierarchy navigation (needs safety check)
=== The Problem ===
All three use identical syntax: (Type)expression
But they do COMPLETELY DIFFERENT things:
1. Value conversion (modifies bits)
2. Reinterpretation (keeps bits, changes view)
3. Type hierarchy navigation (needs runtime check)
Solution: Use specific cast operators!
Explanation:
- Same syntax, different semantics:
(int)12.34performs arithmetic conversion(long)pxreinterprets pointer bits(Base*)pdnavigates type hierarchy
- Why this is problematic:
- Code reader cannot tell intent
- Compiler cannot provide appropriate warnings
- Hard to search for specific cast types in codebase
- Maintenance difficulty
- The C++ solution:
static_cast<int>(12.34)- explicit value conversionreinterpret_cast<long>(px)- explicit reinterpretationdynamic_cast<Derived*>(pb)- safe type hierarchy navigation
Answer: C-style casts hide programmer intent. C++ provides four specialized cast operators with clear semantics: static_cast, dynamic_cast, const_cast, and reinterpret_cast.
3.7. Dynamic Cast in Action (Tutorial 4, Example 2)
Demonstrate safe downcasting using dynamic_cast with runtime type checking.
Click to see the solution
Key Concept: dynamic_cast performs runtime checks, returning nullptr if the cast is invalid, preventing undefined behavior.
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual ~Base() { }
};
class Derived : public Base {
public:
void f() override { cout << "Derived::f" << endl; }
void derivedMethod() { cout << "Derived-specific method" << endl; }
};
int main() {
cout << "=== C-Style Cast: DANGEROUS ===" << endl;
Base* pb1 = new Base(); // Actually points to Base, not Derived
// C-style cast: NO RUNTIME CHECK
Derived* pd1 = (Derived*)pb1;
cout << "C-style cast succeeded (no checks performed)" << endl;
// pd1->derivedMethod(); // UNDEFINED BEHAVIOR! pb1 doesn't point to Derived
cout << "Calling derivedMethod() would cause undefined behavior!" << endl;
cout << "\n=== Dynamic Cast: SAFE ===" << endl;
Base* pb2 = new Base(); // Actually points to Base, not Derived
// Dynamic cast: PERFORMS RUNTIME CHECK
Derived* pd2 = dynamic_cast<Derived*>(pb2);
if (pd2 != nullptr) {
cout << "Cast succeeded" << endl;
pd2->derivedMethod();
} else {
cout << "Cast failed: pb2 doesn't point to Derived (returned nullptr)" << endl;
cout << "Safe! No undefined behavior." << endl;
}
cout << "\n=== Dynamic Cast: Success Case ===" << endl;
Base* pb3 = new Derived(); // Actually points to Derived
Derived* pd3 = dynamic_cast<Derived*>(pb3);
if (pd3 != nullptr) {
cout << "Cast succeeded: pb3 actually points to Derived" << endl;
pd3->derivedMethod();
} else {
cout << "Cast failed" << endl;
}
cout << "\n=== Comparison ===" << endl;
cout << "C-style cast: Fast but UNSAFE (no checks)" << endl;
cout << "dynamic_cast: Slower but SAFE (runtime checks)" << endl;
cout << " - Returns nullptr if cast fails (for pointers)" << endl;
cout << " - Throws bad_cast if cast fails (for references)" << endl;
cout << " - Requires at least one virtual function in base" << endl;
delete pb1;
delete pb2;
delete pb3;
return 0;
}Output:
=== C-Style Cast: DANGEROUS ===
C-style cast succeeded (no checks performed)
Calling derivedMethod() would cause undefined behavior!
=== Dynamic Cast: SAFE ===
Cast failed: pb2 doesn't point to Derived (returned nullptr)
Safe! No undefined behavior.
=== Dynamic Cast: Success Case ===
Cast succeeded: pb3 actually points to Derived
Derived-specific method
=== Comparison ===
C-style cast: Fast but UNSAFE (no checks)
dynamic_cast: Slower but SAFE (runtime checks)
- Returns nullptr if cast fails (for pointers)
- Throws bad_cast if cast fails (for references)
- Requires at least one virtual function in base
Explanation:
- C-style cast danger:
- Always succeeds at compile time
- No runtime validation
- Can create invalid pointers leading to undefined behavior
- dynamic_cast safety:
- Checks actual object type at runtime
- Returns
nullptrif object is not of target type - Prevents undefined behavior
- Requirements:
- Base class must have at least one virtual function (enables RTTI)
- Slightly slower due to runtime check
- When to use:
- When you’re not 100% sure of the actual type
- When safety is more important than performance
- When working with polymorphic hierarchies
Answer: dynamic_cast provides runtime type safety by checking if the conversion is valid, returning nullptr for invalid pointer casts instead of causing undefined behavior.
3.8. Static Cast for Known-Safe Conversions (Tutorial 4, Example 3)
Demonstrate when and why to use static_cast for compile-time conversions.
Click to see the solution
Key Concept: static_cast is faster than dynamic_cast but requires programmer certainty about type safety.
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual ~Base() { }
};
class Derived : public Base {
public:
void f() override { cout << "Derived::f" << endl; }
void derivedMethod() { cout << "Derived method" << endl; }
};
int main() {
cout << "=== Use Case 1: Numeric Conversions ===" << endl;
double d = 3.14159;
int i = static_cast<int>(d); // Explicit about data loss
cout << "static_cast<int>(3.14159) = " << i << endl;
cout << "Explicit: programmer acknowledges data loss" << endl;
cout << "\n=== Use Case 2: Upcasting (Always Safe) ===" << endl;
Derived* pd = new Derived();
Base* pb = static_cast<Base*>(pd); // Derived → Base (safe)
cout << "Upcasted Derived* to Base* (always safe)" << endl;
pb->f(); // Polymorphic call
cout << "\n=== Use Case 3: Downcasting (ONLY if certain) ===" << endl;
Base* pb2 = new Derived(); // We KNOW it points to Derived
// Safe because we're certain pb2 points to Derived
Derived* pd2 = static_cast<Derived*>(pb2);
cout << "Downcasted Base* to Derived* (we're certain of type)" << endl;
pd2->derivedMethod();
cout << "\n=== Comparison with dynamic_cast ===" << endl;
cout << "static_cast: No runtime overhead, no safety checks" << endl;
cout << "dynamic_cast: Runtime overhead, but provides safety" << endl;
cout << "\nWhen to use static_cast:" << endl;
cout << " 1. You're 100% certain of the actual type" << endl;
cout << " 2. Performance is critical" << endl;
cout << " 3. Numeric conversions (explicit data loss)" << endl;
cout << "\n=== DANGER: Wrong Use ===" << endl;
Base* pb3 = new Base(); // Points to Base, NOT Derived
Derived* pd3 = static_cast<Derived*>(pb3); // WRONG! No check performed
cout << "Cast succeeded but UNSAFE - pd3 doesn't point to valid Derived" << endl;
// pd3->derivedMethod(); // UNDEFINED BEHAVIOR!
cout << "Would cause undefined behavior if we call derived methods" << endl;
delete pb;
delete pb2;
delete pb3;
return 0;
}Output:
=== Use Case 1: Numeric Conversions ===
static_cast<int>(3.14159) = 3
Explicit: programmer acknowledges data loss
=== Use Case 2: Upcasting (Always Safe) ===
Upcasted Derived* to Base* (always safe)
Derived::f
=== Use Case 3: Downcasting (ONLY if certain) ===
Downcasted Base* to Derived* (we're certain of type)
Derived method
=== Comparison with dynamic_cast ===
static_cast: No runtime overhead, no safety checks
dynamic_cast: Runtime overhead, but provides safety
When to use static_cast:
1. You're 100% certain of the actual type
2. Performance is critical
3. Numeric conversions (explicit data loss)
=== DANGER: Wrong Use ===
Cast succeeded but UNSAFE - pd3 doesn't point to valid Derived
Would cause undefined behavior if we call derived methods
Explanation:
- static_cast advantages:
- No runtime overhead
- Faster than
dynamic_cast - Explicit about intent
- Safe uses:
- Upcasting (always safe by type system)
- Numeric conversions (explicit data loss)
- Downcasting when you’re absolutely certain
- Dangerous uses:
- Downcasting without certainty
- Can cause undefined behavior
- No safety net
- Rule of thumb:
- Use
dynamic_castby default for downcasting - Use
static_castonly when performance is critical AND you’re certain - Always prefer safety over speed unless profiling proves otherwise
- Use
Answer: static_cast performs compile-time conversions without runtime checks. Use for safe upcasts, explicit numeric conversions, and performance-critical downcasts where type is guaranteed. Prefer dynamic_cast when type certainty is questionable.
3.9. Const Cast for Const Correctness (Tutorial 4, Example 4)
Demonstrate when const_cast is necessary and when it’s dangerous.
Click to see the solution
Key Concept: const_cast removes const qualification but should be used sparingly and only when you’re certain the function won’t actually modify the data.
#include <iostream>
#include <cstring>
using namespace std;
// Legacy function that doesn't use const correctly
void legacyPrint(char* str) {
// This function only reads, doesn't modify
cout << "String: " << str << endl;
}
// Another legacy function (pretend it's from old C library)
void legacyProcess(char* str) {
// Actually modifies the string!
for (size_t i = 0; i < strlen(str); i++) {
str[i] = toupper(str[i]);
}
}
int main() {
cout << "=== Safe Use: Read-Only Legacy API ===" << endl;
const char* message = "Hello, World!";
// Error without const_cast:
// legacyPrint(message); // ERROR: can't pass const char* to char*
// OK with const_cast (safe because legacyPrint doesn't modify)
legacyPrint(const_cast<char*>(message));
cout << "Safe: legacyPrint only reads the string" << endl;
cout << "\n=== UNSAFE Use: Modifying Legacy API ===" << endl;
const char* constMessage = "Dangerous";
cout << "Before: " << constMessage << endl;
// DANGEROUS! legacyProcess actually modifies the string
// legacyProcess(const_cast<char*>(constMessage)); // UNDEFINED BEHAVIOR!
cout << "Cannot safely use const_cast here - legacyProcess modifies data" << endl;
cout << "Would cause undefined behavior (string literal is in read-only memory)" << endl;
cout << "\n=== Safe Alternative: Copy First ===" << endl;
char buffer[100];
strcpy(buffer, "Hello");
cout << "Before: " << buffer << endl;
const char* constPtr = buffer; // Now const
char* mutablePtr = const_cast<char*>(constPtr); // Remove const
legacyProcess(mutablePtr); // Safe: original wasn't const
cout << "After: " << buffer << endl;
cout << "\n=== Best Practice ===" << endl;
cout << "1. Avoid const_cast when possible" << endl;
cout << "2. Only use when interfacing with legacy APIs" << endl;
cout << "3. Ensure the underlying object wasn't originally const" << endl;
cout << "4. Document why const_cast is necessary" << endl;
cout << "5. Consider wrapping legacy API with const-correct interface" << endl;
return 0;
}Output:
=== Safe Use: Read-Only Legacy API ===
String: Hello, World!
Safe: legacyPrint only reads the string
=== UNSAFE Use: Modifying Legacy API ===
Before: Dangerous
Cannot safely use const_cast here - legacyProcess modifies data
Would cause undefined behavior (string literal is in read-only memory)
=== Safe Alternative: Copy First ===
Before: Hello
After: HELLO
=== Best Practice ===
1. Avoid const_cast when possible
2. Only use when interfacing with legacy APIs
3. Ensure the underlying object wasn't originally const
4. Document why const_cast is necessary
5. Consider wrapping legacy API with const-correct interface
Explanation:
- Safe usage:
- Removing const to call read-only function with incorrect signature
- Original object wasn’t truly const (was created as non-const)
- Unsafe usage:
- Removing const from string literal (undefined behavior if modified)
- Removing const from truly const object
- The key rule:
- Safe if: Object was created non-const, just viewed through const pointer/reference
- Unsafe if: Object was originally created as const
- Why this matters:
- Const objects may be placed in read-only memory
- Modifying them causes segmentation fault or silent corruption
- Compiler optimizations assume const objects don’t change
Better solution than const_cast:
// Instead of using const_cast, fix the API:
void properPrint(const char* str) { // Now const-correct
cout << "String: " << str << endl;
}
// Or create a wrapper:
void safeLegacyPrint(const char* str) {
legacyPrint(const_cast<char*>(str)); // Encapsulate the cast
}Answer: const_cast removes const qualification. Safe only when: 1) interfacing with legacy APIs, 2) function doesn’t actually modify data, 3) original object wasn’t truly const. Prefer fixing API over using const_cast.
3.10. Reinterpret Cast for Low-Level Operations (Tutorial 4, Example 5)
Demonstrate reinterpret_cast for low-level bit manipulation and pointer storage.
Click to see the solution
Key Concept: reinterpret_cast reinterprets bit patterns without changing them - the most dangerous cast, reserved for systems programming.
#include <iostream>
using namespace std;
int main() {
cout << "=== Use Case 1: Pointer ↔ Integer Conversion ===" << endl;
int x = 777;
int* p = &x;
cout << "Original pointer: " << p << endl;
cout << "Points to value: " << *p << endl;
// Store pointer as integer (e.g., for hashing, debugging)
long ptrAsInt = reinterpret_cast<long>(p);
cout << "Pointer as long: " << ptrAsInt << endl;
// Convert back to pointer
int* pBack = reinterpret_cast<int*>(ptrAsInt);
cout << "Converted back: " << pBack << endl;
cout << "Still points to: " << *pBack << endl;
cout << "\n=== Use Case 2: Type Punning (View Memory Differently) ===" << endl;
unsigned int bits = 0x3F800000; // IEEE 754 representation of 1.0f
cout << "As unsigned int: " << bits << endl;
// View these bits as a float
float* fptr = reinterpret_cast<float*>(&bits);
cout << "Same bits as float: " << *fptr << endl;
cout << "\n=== Use Case 3: Incompatible Pointer Types ===" << endl;
unsigned int ux = 777;
unsigned int* pux = &ux;
// This would be an error without cast:
// int* pix = pux; // ERROR: incompatible types
// reinterpret_cast allows it
int* pix = reinterpret_cast<int*>(pux);
cout << "unsigned* converted to int*" << endl;
cout << "Value: " << *pix << endl;
cout << "\n=== DANGER: Platform-Specific ===" << endl;
cout << "sizeof(void*) = " << sizeof(void*) << endl;
cout << "sizeof(long) = " << sizeof(long) << endl;
if (sizeof(void*) != sizeof(long)) {
cout << "WARNING: Pointer-to-long conversion may lose data!" << endl;
cout << "Use intptr_t or uintptr_t from <cstdint> instead" << endl;
} else {
cout << "OK: Pointer fits in long on this platform" << endl;
}
cout << "\n=== When to Use reinterpret_cast ===" << endl;
cout << "1. Low-level memory operations" << endl;
cout << "2. Hardware register access" << endl;
cout << "3. Binary serialization/deserialization" << endl;
cout << "4. Implementing custom memory allocators" << endl;
cout << "5. Interfacing with C APIs using void*" << endl;
cout << "\nFor application code: Almost NEVER!" << endl;
cout << "\n=== Better Alternatives ===" << endl;
cout << "• For pointer storage: use uintptr_t (from <cstdint>)" << endl;
cout << "• For type punning: use union or memcpy (safer)" << endl;
cout << "• For different pointer types: redesign to avoid need" << endl;
return 0;
}Output:
=== Use Case 1: Pointer ↔ Integer Conversion ===
Original pointer: 0x7ffeefbff5ac
Points to value: 777
Pointer as long: 140732920755372
Converted back: 0x7ffeefbff5ac
Still points to: 777
=== Use Case 2: Type Punning (View Memory Differently) ===
As unsigned int: 1065353216
Same bits as float: 1
=== Use Case 3: Incompatible Pointer Types ===
unsigned* converted to int*
Value: 777
=== DANGER: Platform-Specific ===
sizeof(void*) = 8
sizeof(long) = 8
OK: Pointer fits in long on this platform
=== When to Use reinterpret_cast ===
1. Low-level memory operations
2. Hardware register access
3. Binary serialization/deserialization
4. Implementing custom memory allocators
5. Interfacing with C APIs using void*
For application code: Almost NEVER!
=== Better Alternatives ===
• For pointer storage: use uintptr_t (from <cstdint>)
• For type punning: use union or memcpy (safer)
• For different pointer types: redesign to avoid need
Explanation:
- What it does:
- Reinterprets bit pattern without modification
- No conversion, just different view
- Bypasses type system completely
- Common uses:
- Storing pointers as integers
- Type punning (viewing same memory as different type)
- Converting between incompatible pointer types
- Dangers:
- Platform-specific behavior
- Alignment issues
- Undefined behavior if misused
- Breaks type safety
- Why it’s dangerous:
- Can violate strict aliasing rules
- May cause misaligned access (crashes on some platforms)
- Endianness issues
- Pointer size assumptions
Safer alternatives:
#include <cstdint>
// Instead of reinterpret_cast<long>(ptr):
uintptr_t ptrAsInt = reinterpret_cast<uintptr_t>(ptr); // Guaranteed to fit
// Instead of reinterpret_cast for type punning:
union FloatInt {
float f;
uint32_t i;
};
FloatInt fi;
fi.i = 0x3F800000;
float value = fi.f; // Safer than reinterpret_castAnswer: reinterpret_cast reinterprets bits without conversion. Use ONLY for: systems programming, hardware access, or binary serialization. For application code, prefer safer alternatives like uintptr_t or unions.
3.11. Using typeid for Type Identification (Tutorial 4, Example 6)
Demonstrate using typeid to identify object types at runtime and compare types in a polymorphic hierarchy.
Click to see the solution
Key Concept: typeid provides runtime type information, useful for type checking and debugging polymorphic hierarchies.
#include <iostream>
#include <typeinfo>
using namespace std;
class Animal {
public:
virtual ~Animal() { } // Virtual destructor (required for RTTI)
};
class Dog : public Animal {
public:
void bark() { cout << "Woof!" << endl; }
};
class Cat : public Animal {
public:
void meow() { cout << "Meow!" << endl; }
};
void identifyAnimal(Animal* a) {
cout << "Type: " << typeid(*a).name() << endl;
// Compare with specific types
if (typeid(*a) == typeid(Dog)) {
cout << "This is a Dog" << endl;
Dog* d = static_cast<Dog*>(a); // Safe because we checked
d->bark();
} else if (typeid(*a) == typeid(Cat)) {
cout << "This is a Cat" << endl;
Cat* c = static_cast<Cat*>(a); // Safe because we checked
c->meow();
} else if (typeid(*a) == typeid(Animal)) {
cout << "This is a generic Animal" << endl;
}
}
int main() {
Dog dog;
Cat cat;
Animal animal;
Animal* ptr;
// Test with different objects
cout << "=== Testing with Dog ===" << endl;
ptr = &dog;
identifyAnimal(ptr);
cout << "\n=== Testing with Cat ===" << endl;
ptr = &cat;
identifyAnimal(ptr);
cout << "\n=== Testing with Animal ===" << endl;
ptr = &animal;
identifyAnimal(ptr);
// Demonstrating difference between static and dynamic type
cout << "\n=== Static vs Dynamic Type ===" << endl;
Animal* ptrToDog = new Dog();
cout << "Static type (pointer): " << typeid(ptrToDog).name() << endl;
cout << "Dynamic type (object): " << typeid(*ptrToDog).name() << endl;
// Can use this for conditional behavior
if (typeid(*ptrToDog) == typeid(Dog)) {
cout << "Confirmed: pointer points to a Dog" << endl;
}
delete ptrToDog;
return 0;
}Possible Output:
=== Testing with Dog ===
Type: 3Dog
This is a Dog
Woof!
=== Testing with Cat ===
Type: 3Cat
This is a Cat
Meow!
=== Testing with Animal ===
Type: 6Animal
This is a generic Animal
=== Static vs Dynamic Type ===
Static type (pointer): P6Animal
Dynamic type (object): 3Dog
Confirmed: pointer points to a Dog
Note: The exact output of type_info::name() is implementation-defined and may look different on your compiler (e.g., “Dog”, “class Dog”, or mangled names).
Explanation:
- Basic usage:
typeid(*ptr)gets the dynamic type of the object - Type comparison:
typeid(obj) == typeid(Type)checks if object is of specific type - Static vs Dynamic:
typeid(ptr)gives type of the pointer (Animal*)typeid(*ptr)gives type of the object (actual type likeDog)
- Practical use: Can identify type and then safely cast to perform type-specific operations
Alternative using dynamic_cast (generally preferred):
void identifyAnimal(Animal* a) {
if (Dog* d = dynamic_cast<Dog*>(a)) {
cout << "This is a Dog" << endl;
d->bark();
} else if (Cat* c = dynamic_cast<Cat*>(a)) {
cout << "This is a Cat" << endl;
c->meow();
}
}Answer: typeid returns type_info reference for runtime type checking. Use typeid(*ptr) for dynamic type, typeid(ptr) for static type. Compare with == operator.